<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <title>多端同步视频播放器</title>
  <link href="https://vjs.zencdn.net/7.14.3/video-js.css" rel="stylesheet" />
  <style>
    /* 基础样式保持不变 */
    .container {
      max-width: 1200px;
      margin: 20px auto;
      padding: 0 15px;
    }
    .video-container {
      width: 100%;
      border: 1px solid #ddd;
      border-radius: 8px;
      overflow: hidden;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }

    /* 移动端控制区域优化 */
    .controls {
      margin-top: 15px;
      display: flex;
      gap: 10px;
      align-items: center;
      flex-wrap: wrap;
      padding: 10px;
      background-color: #f8f9fa;
      border-radius: 4px;
    }
    input[type="text"] {
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      flex: 1;
      min-width: 250px;
      font-size: 14px;
    }
    button {
      padding: 10px 16px;
      background: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      transition: background 0.3s;
      font-size: 14px;
      /* 移动端按钮增大点击区域 */
      min-height: 44px;
      min-width: 44px;
    }
    /* 移动端适配样式 */
    @media (max-width: 768px) {
      .controls {
        gap: 8px;
        padding: 8px;
      }
      button, .format-selector, input[type="text"] {
        font-size: 13px;
        padding: 8px 12px;
        min-height: 40px;
      }
      .sync-options {
        flex-direction: column;
        align-items: flex-start;
        padding: 10px 15px;
      }
      .sync-options label {
        margin-bottom: 8px;
        font-size: 13px;
      }
    }

    .format-selector {
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 14px;
    }
    .status-message {
      margin-top: 10px;
      padding: 10px;
      border-radius: 4px;
      display: none;
    }
    .error {
      background-color: #f8d7da;
      color: #721c24;
    }
    .info {
      background-color: #d1ecf1;
      color: #0c5460;
    }
    .video-js {
      width: 100%;
      height: auto;
    }
    .sync-options {
      margin-top: 10px;
      padding: 10px;
      background-color: #f8f9fa;
      border-radius: 4px;
      display: flex;
      align-items: center;
      gap: 15px;
    }
    .sync-options label {
      display: flex;
      align-items: center;
      gap: 5px;
      color: #333;
    }
    .player-info {
      margin-top: 10px;
      color: #666;
      font-size: 0.9em;
    }
    /* 移动端加载指示器 */
    .mobile-loader {
      display: none;
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: rgba(0,0,0,0.7);
      color: white;
      padding: 15px 20px;
      border-radius: 8px;
      z-index: 1000;
    }
  </style>
</head>

<body>
  <div class="container">
    <h1>多端同步视频播放器</h1>

    <!-- 视频播放器区域 -->
    <div class="video-container">
      <video id="videoPlayer" class="video-js vjs-default-skin vjs-big-play-centered" controls>
        您的浏览器不支持HTML5视频
      </video>
    </div>

    <!-- 移动端加载指示器 -->
    <div class="mobile-loader" id="mobileLoader">同步中...</div>

    <!-- 状态消息区域 -->
    <div id="statusMessage" class="status-message"></div>

    <!-- 控制区域 -->
    <div class="controls">
      <select id="formatSelector" class="format-selector">
        <option value="auto">自动检测</option>
        <option value="mp4">MP4格式</option>
        <option value="m3u8">M3U8格式 (HLS)</option>
        <option value="webm">WebM格式</option>
        <option value="ogg">OGG格式</option>
      </select>
      <input type="text" id="videoUrl" placeholder="输入视频URL">
      <button onclick="window.playerActions.loadVideo()">加载视频</button>
      <button onclick="window.playerActions.togglePlay()">播放/暂停</button>
      <button onclick="window.playerActions.syncNow()">立即同步</button>
    </div>

    <!-- 同步选项 -->
    <div class="sync-options">
      <label><input type="checkbox" id="syncPlayPause" checked> 同步播放/暂停</label>
      <label><input type="checkbox" id="syncProgress" checked> 同步播放进度</label>
      <label><input type="checkbox" id="syncVolume" checked> 同步音量</label>
      <label><input type="checkbox" id="syncOnLoad" checked> 加载后自动同步</label>
      <!-- 新增移动端选项 -->
      <label><input type="checkbox" id="mobileOptimized" checked> 移动端优化模式</label>
    </div>

    <div class="player-info">
      <span>连接状态: </span><span id="connectionStatus">未连接</span>
      <span style="margin-left: 15px;">当前观看人数: </span><span id="viewerCount">0</span>
      <span style="margin-left: 15px;">同步延迟: </span><span id="syncDelay">0ms</span>
    </div>
  </div>

  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
  <script src="https://vjs.zencdn.net/7.14.3/video.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/videojs-contrib-hls@5.15.0/dist/videojs-contrib-hls.min.js"></script>

  <script>
    // 解决插件冲突
    if (window.videojs && typeof window.videojs.unregisterPlugin === 'function') {
      if (window.videojs.getPlugin('reloadSourceOnError')) {
        window.videojs.unregisterPlugin('reloadSourceOnError');
      }
    }

    window.playerActions = {
      player: null,
      socket: null,
      syncOptions: {},
      isSyncing: false,
      lastSyncTime: 0,
      serverTimeOffset: 0, // 服务器时间偏移量
      heartbeatInterval: null, // 心跳计时器
      retryCount: 0, // 重连计数器
      maxRetries: 5, // 最大重连次数

      init: function() {
        // 获取DOM元素引用
        this.statusMessage = document.getElementById('statusMessage');
        this.videoUrlInput = document.getElementById('videoUrl');
        this.formatSelector = document.getElementById('formatSelector');
        this.connectionStatus = document.getElementById('connectionStatus');
        this.viewerCount = document.getElementById('viewerCount');
        this.syncDelay = document.getElementById('syncDelay');
        this.mobileLoader = document.getElementById('mobileLoader');

        // 同步选项
        this.syncOptions = {
          playPause: document.getElementById('syncPlayPause'),
          progress: document.getElementById('syncProgress'),
          volume: document.getElementById('syncVolume'),
          syncOnLoad: document.getElementById('syncOnLoad'),
          mobileOptimized: document.getElementById('mobileOptimized')
        };

        // 初始化视频播放器
        this.initPlayer();

        // 连接WebSocket
        this.initSocket();

        // 检测是否为移动设备
        this.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
        if (this.isMobile) {
          this.showMessage('已启用移动端优化模式');
        }
      },

      // 改进的播放器初始化方法（移动端优化）
      initPlayer: function() {
        try {
          const videoElement = document.getElementById('videoPlayer');
          if (!videoElement) {
            throw new Error('视频元素未找到');
          }

          // 移动端特殊配置
          const mobileConfig = this.isMobile ? {
            controls: true,
            responsive: true,
            fluid: true,
            inactivityTimeout: 2000, // 移动端控制栏消失更快
            html5: {
              hls: {
                maxBufferLength: 15, // 移动端缓冲减少
                maxMaxBufferLength: 120,
                enableWorker: false,
                lowLatencyMode: true // 低延迟模式
              }
            }
          } : {
            autoplay: false,
            controls: true,
            responsive: true,
            fluid: true,
            html5: {
              hls: {
                maxBufferLength: 30,
                maxMaxBufferLength: 600,
                enableWorker: false
              }
            }
          };

          // 初始化播放器
          this.player = window.videojs('videoPlayer', mobileConfig);

          // 验证播放器是否初始化成功
          if (!this.player || typeof this.player.on !== 'function') {
            throw new Error('播放器初始化不完整，缺少必要方法');
          }

          // 绑定播放器事件
          this.bindPlayerEvents();

          // 检查HLS支持
          if (!this.player.hls) {
            this.showMessage('HLS插件未正确加载，M3U8视频可能无法播放', true);
          }
        } catch (e) {
          this.showMessage('播放器初始化失败: ' + e.message, true);
          console.error('播放器初始化错误:', e);
        }
      },

      initSocket: function() {
        try {
          // 移动端使用更稳定的连接方式
          const options = this.isMobile ? {
            reconnection: true,
            reconnectionAttempts: 5,
            reconnectionDelay: 1000,
            timeout: 10000,
            autoConnect: true
          } : {};

          this.socket = io.connect('http://' + document.domain + ':' + location.port, options);

          // 连接事件
          this.socket.on('connect', () => {
            this.connectionStatus.textContent = '已连接';
            this.showMessage('已连接到同步服务器');
            this.retryCount = 0; // 重置重连计数器

            // 启动心跳机制
            this.startHeartbeat();

            // 请求服务器时间进行校准
            this.socket.emit('get_server_time', (serverTime) => {
              const localTime = Date.now();
              this.serverTimeOffset = serverTime - localTime;
            });
          });

          // 断开连接事件
          this.socket.on('disconnect', (reason) => {
            this.connectionStatus.textContent = '已断开';
            this.showMessage(`连接已断开: ${reason}，正在尝试重连...`, true);

            // 停止心跳
            this.stopHeartbeat();

            // 自动重连
            if (reason === 'io server disconnect' && this.retryCount < this.maxRetries) {
              this.socket.connect();
              this.retryCount++;
            }
          });

          // 重连事件
          this.socket.on('reconnect', (attempt) => {
            this.showMessage(`第 ${attempt} 次重连成功`);
            this.connectionStatus.textContent = '已连接';
          });

          // 接收观看人数
          this.socket.on('viewer_count', (count) => {
            this.viewerCount.textContent = count;
          });

          // 接收同步状态
          this.socket.on('sync_state', (state) => {
            // 计算同步延迟
            const now = Date.now() + this.serverTimeOffset;
            const delay = now - state.timestamp;
            this.syncDelay.textContent = `${delay}ms`;

            // 移动端延迟补偿
            if (this.isMobile && this.syncOptions.mobileOptimized.checked) {
              // 对进度进行延迟补偿
              if (state.status === 'play' && state.progress) {
                const补偿进度 = state.progress + (delay / 1000);
                state.progress = 补偿进度;
              }
            }

            this.handleSyncState(state);
          });

          // 连接错误
          this.socket.on('connect_error', (error) => {
            this.showMessage(`连接错误: ${error.message}`, true);
          });

        } catch (e) {
          this.showMessage('Socket连接初始化失败: ' + e.message, true);
          console.error('Socket错误:', e);
        }
      },

      // 启动心跳机制
      startHeartbeat: function() {
        // 每5秒发送一次心跳
        this.stopHeartbeat(); // 先停止已有的心跳
        this.heartbeatInterval = setInterval(() => {
          if (this.socket && this.socket.connected) {
            this.socket.emit('heartbeat', (response) => {
              if (!response) {
                console.warn('心跳无响应');
              }
            });
          }
        }, 5000);
      },

      // 停止心跳机制
      stopHeartbeat: function() {
        if (this.heartbeatInterval) {
          clearInterval(this.heartbeatInterval);
          this.heartbeatInterval = null;
        }
      },

      bindPlayerEvents: function() {
        const self = this;

        if (!this.player || typeof this.player.on !== 'function') {
          this.showMessage('无法绑定事件，播放器未正确初始化', true);
          return;
        }

        // 进度更新事件 - 移动端降低更新频率
        let lastSyncTime = 0;
        const syncInterval = this.isMobile ? 1000 : 500; // 移动端1秒一次，桌面500ms一次

        this.player.on('timeupdate', () => {
          if (self.syncOptions.progress.checked && !self.isSyncing) {
            const now = Date.now();
            if (now - lastSyncTime > syncInterval) {
              lastSyncTime = now;
              self.syncToServer();
            }
          }
        });

        // 播放/暂停事件
        this.player.on('play', () => {
          if (self.syncOptions.playPause.checked && !self.isSyncing) {
            self.syncToServer();
          }
        });

        this.player.on('pause', () => {
          if (self.syncOptions.playPause.checked && !self.isSyncing) {
            self.syncToServer();
          }
        });

        // 音量变化事件 - 移动端减少触发频率
        let lastVolumeSync = 0;
        this.player.on('volumechange', () => {
          if (self.syncOptions.volume.checked && !self.isSyncing) {
            const now = Date.now();
            if (now - lastVolumeSync > 1000) { // 音量同步1秒一次
              lastVolumeSync = now;
              self.syncToServer();
            }
          }
        });

        // 错误处理增强
        this.player.on('error', () => {
          const error = self.player.error();
          let errorMessage = '视频播放错误: ';

          switch(error.code) {
            case 1: errorMessage += '视频加载被中止'; break;
            case 2: errorMessage += '网络错误，请检查连接'; break;
            case 3: errorMessage += '视频解码错误'; break;
            case 4: errorMessage += '不支持的视频格式或编码'; break;
            default: errorMessage += `错误代码: ${error.code}, 消息: ${error.message}`;
          }

          self.showMessage(errorMessage, true);

                    // 移动端自动重试机制
          if (self.isMobile && [1, 2].includes(error.code)) {
            setTimeout(() => {
              self.showMessage('尝试重新加载视频...');
              self.player.reload();
            }, 3000);
          }
        });

        // 移动端触摸事件优化
        if (this.isMobile) {
          const videoContainer = document.querySelector('.video-container');
          // 双击播放/暂停
          videoContainer.addEventListener('dblclick', (e) => {
            e.preventDefault();
            self.togglePlay();
          });

          // 触摸时显示控制栏
          videoContainer.addEventListener('touchstart', () => {
            if (self.player) {
              self.player.userActive(true);
              setTimeout(() => {
                self.player.userActive(false);
              }, 3000);
            }
          });
        }
      },

      showMessage: function(text, isError = false) {
        if (!this.statusMessage) return;

        this.statusMessage.textContent = text;
        this.statusMessage.className = 'status-message ' + (isError ? 'error' : 'info');
        this.statusMessage.style.display = 'block';

        // 移动端消息显示时间更长
        const timeout = this.isMobile ? 5000 : 3000;
        if (!isError) {
          setTimeout(() => {
            if (this.statusMessage) {
              this.statusMessage.style.display = 'none';
            }
          }, timeout);
        }
      },

      handleSyncState: function(state) {
        if (this.isSyncing || !this.player) return;

        // 移动端显示同步指示器
        if (this.isMobile) {
          this.mobileLoader.style.display = 'block';
        }

        this.isSyncing = true;

        try {
          // 视频源同步
          if (this.player.src() !== state.video_url && state.video_url) {
            this.loadVideoFromUrl(state.video_url, state.format, false);
            this.videoUrlInput.value = state.video_url;
            this.formatSelector.value = state.format || 'auto';
            this.showMessage('已同步视频源');
          }

          // 进度同步 - 移动端放宽同步阈值
          const progressThreshold = this.isMobile ? 1.5 : 0.5;
          if (this.syncOptions.progress.checked &&
              Math.abs(this.player.currentTime() - state.progress) > progressThreshold &&
              state.video_url) {
            this.player.currentTime(state.progress);
          }

          // 播放状态同步
          if (this.syncOptions.playPause.checked && state.video_url) {
            if (state.status === 'play' && this.player.paused()) {
              this.player.play().catch(e => {
                // 移动端自动播放限制处理
                if (this.isMobile) {
                  this.showMessage('请点击播放按钮开始播放', true);
                } else {
                  this.showMessage('自动播放失败，请手动点击播放: ' + e.message, true);
                }
              });
            } else if (state.status === 'pause' && !this.player.paused()) {
              this.player.pause();
            }
          }

          // 音量同步
          if (this.syncOptions.volume.checked &&
              Math.abs(this.player.volume() - state.volume) > 0.05) {
            this.player.volume(state.volume);
            this.player.muted(state.muted);
          }
        } finally {
          // 隐藏移动端同步指示器
          if (this.isMobile) {
            setTimeout(() => {
              this.mobileLoader.style.display = 'none';
            }, 500);
          }

          // 移动端延长同步锁定期，避免频繁同步冲突
          const unlockDelay = this.isMobile ? 800 : 500;
          setTimeout(() => {
            this.isSyncing = false;
          }, unlockDelay);
        }
      },

      syncToServer: function() {
        if (this.isSyncing || !this.socket || !this.player || !this.socket.connected) return;

        // 移动端精简同步数据，减少传输量
        const state = {
          progress: this.player.currentTime(),
          status: this.player.paused() ? 'pause' : 'play',
          video_url: this.player.src(),
          format: this.formatSelector.value,
          // 移动端只在音量变化明显时同步
          volume: this.syncOptions.volume.checked ? this.player.volume() : undefined,
          muted: this.syncOptions.volume.checked ? this.player.muted() : undefined,
          timestamp: Date.now() + this.serverTimeOffset // 带时间戳的同步数据
        };

        // 移动端使用压缩格式发送
        if (this.isMobile && this.syncOptions.mobileOptimized.checked) {
          // 移除undefined字段
          Object.keys(state).forEach(key =>
            state[key] === undefined && delete state[key]
          );
        }

        this.socket.emit('update_state', state);
      },

      syncNow: function() {
        if (this.isMobile) {
          this.mobileLoader.style.display = 'block';
          setTimeout(() => {
            this.mobileLoader.style.display = 'none';
          }, 1000);
        }
        this.syncToServer();
        this.showMessage('已同步当前播放状态');
      },

      loadVideoFromUrl: function(url, format, shouldSync = true) {
        if (!this.player) {
          this.showMessage('播放器未初始化', true);
          return false;
        }

        // 移动端加载时显示指示器
        if (this.isMobile) {
          this.mobileLoader.style.display = 'block';
        }

        try {
          this.player.pause();

          let type;
          if (format === 'auto') {
            if (url.toLowerCase().endsWith('.m3u8')) {
              type = 'application/x-mpegURL';
            } else if (url.toLowerCase().endsWith('.webm')) {
              type = 'video/webm';
            } else if (url.toLowerCase().endsWith('.ogg')) {
              type = 'video/ogg';
            } else {
              type = 'video/mp4';
            }
          } else {
            switch(format) {
              case 'm3u8': type = 'application/x-mpegURL'; break;
              case 'webm': type = 'video/webm'; break;
              case 'ogg': type = 'video/ogg'; break;
              default: type = 'video/mp4';
            }
          }

          const self = this;

          // 移除旧的事件监听器
          if (this.player.off) {
            this.player.off('loadedmetadata', this.onMetadataLoaded);
          }

          // 加载完成回调
          this.onMetadataLoaded = function() {
            self.showMessage('视频加载成功');
            if (shouldSync && self.syncOptions.syncOnLoad.checked) {
              self.syncToServer();
            }
            // 隐藏移动端加载指示器
            if (self.isMobile) {
              self.mobileLoader.style.display = 'none';
            }
            // 移除事件监听器
            if (self.player.off) {
              self.player.off('loadedmetadata', self.onMetadataLoaded);
            }
          };

          // 移动端增加加载超时处理
          if (this.isMobile) {
            const loadTimeout = setTimeout(() => {
              self.showMessage('视频加载超时，正在重试...', true);
              self.player.reset();
              self.loadVideoFromUrl(url, format, shouldSync);
            }, 20000); // 20秒超时

            this.player.on('loadedmetadata', () => {
              clearTimeout(loadTimeout);
              this.onMetadataLoaded();
            });
          } else {
            this.player.on('loadedmetadata', this.onMetadataLoaded);
          }

          // 重置并加载视频
          this.player.reset();
          this.player.src({
            src: url,
            type: type
          });

          this.player.load();

          return true;
        } catch (e) {
          this.showMessage('加载视频失败: ' + e.message, true);
          if (this.isMobile) {
            this.mobileLoader.style.display = 'none';
          }
          return false;
        }
      },

      loadVideo: function() {
        const url = this.videoUrlInput.value.trim();
        const format = this.formatSelector.value;

        if (!url) {
          this.showMessage('请输入有效的视频URL', true);
          return;
        }

        try {
          new URL(url);
          this.showMessage('正在加载视频...');
          this.loadVideoFromUrl(url, format);
        } catch (e) {
          this.showMessage('无效的URL格式: ' + e.message, true);
        }
      },

      togglePlay: function() {
        if (!this.player || !this.player.src()) {
          this.showMessage('请先加载视频', true);
          return;
        }

        try {
          if (this.player.paused()) {
            this.player.play().catch(e => {
              this.showMessage('播放失败: ' + e.message, true);
            });
          } else {
            this.player.pause();
          }
        } catch (e) {
          this.showMessage('操作失败: ' + e.message, true);
        }
      }
    };

    // 等待DOM完全加载后初始化
    document.addEventListener('DOMContentLoaded', function() {
      window.playerActions.init();
    });

    // 清理资源
    window.addEventListener('beforeunload', function() {
      if (window.playerActions && window.playerActions.player) {
        window.playerActions.player.dispose();
      }
      if (window.playerActions && window.playerActions.socket) {
        window.playerActions.socket.disconnect();
      }
      if (window.playerActions) {
        window.playerActions.stopHeartbeat();
      }
    });
  </script>
</body>

</html>